

有几种方法可以组织模板源码。本节介绍一种流行的方法:包含模型。

\subsubsubsection{9.1.1\hspace{0.2cm}连接错误}

大多数C和C++开发者的非模板代码大致组织如下:

\begin{itemize}
\item
类和其他类型声明完全放在头文件中。通常，这个文件名扩展名为.hpp(或.h，.H，.hh，.hxx)的文件。

\item
对于全局(非内联)变量和(非内联)函数，只有声明放在头文件中，定义放在实现单元编译文件中。这样的文件通常的展名为.cpp(或.c，.C，.cc，或.cxx)的文件。
\end{itemize}

其使所需的类型定义在整个工程中都可以获得，并避免了链接器中变量和函数的重复定义的错误。

了解了这些习惯，下面的(错误的)程序展示了开始学习模板开发者所抱怨的常见错误。和“普通代码”一样，可以在头文件中声明模板:

\hspace*{\fill} \\ %插入空行
\noindent
\textit{basics/myfirst.hpp}
\begin{lstlisting}[style=styleCXX]
#ifndef MYFIRST_HPP
#define MYFIRST_HPP

// declaration of template
template<typename T>
void printTypeof (T const&);

#endif // MYFIRST_HPP
\end{lstlisting}

printTypeof()是一个辅助函数的声明，打印一些类型信息。函数实现放在另一个CPP文件中:

\hspace*{\fill} \\ %插入空行
\noindent
\textit{basics/myfirst.cpp}
\begin{lstlisting}[style=styleCXX]
#include <iostream>
#include <typeinfo>
#include "myfirst.hpp"

// implementation/definition of template
template<typename T>
void printTypeof (T const& x)
{
	std::cout << typeid(x).name() << ’\n’;
}
\end{lstlisting}

该示例使用typeid操作符打印一个字符串，该字符串描述传递表达式的类型。返回一个静态类型std::type\_info的左值，提供了一个成员name()，显示表达式的类型。C++标准上并没有规定name()必须返回有意义的东西，但在定义良好的C++实现中，应该获得传递给typeid的表达式类型的字符串，该字符串对表达式的类型进行了很好的描述。
 
\begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black]
\hspace*{0.75cm}某些实现中，这个字符串是混乱的(使用参数类型和作用域的名称进行编码，以区别于其他名称)，存在解析器可以将其转换为人类可读的文本。
\end{tcolorbox}

最后，在另一个CPP文件中使用模板，模板声明通过\#include包含:

\hspace*{\fill} \\ %插入空行
\noindent
\textit{basics/myfirstmain.cpp}
\begin{lstlisting}[style=styleCXX]
#include "myfirst.hpp"
// use of the template
int main()
{
	double ice = 3.0;
	printTypeof(ice); // call function template for type double
}
\end{lstlisting}

C++编译器可能会接受这个程序，但是链接器可能会报错:printTypeof()函数没有定义。

这个错误的原因是函数模板printTypeof()的定义没有实例化。为了实例化模板，编译器必须知道应该实例化哪个定义，以及应该为哪个模板参数实例化它。但前面的示例中，这两个信息在单独编译的文件中。因此，当编译器看到对printTypeof()的调用，但没有看到为double实例化这个函数的定义时，只能假设在其他地方提供了这样的定义，并创建了对该定义的引用(供链接器解析)。另一方面，当编译器处理myfirst.cpp文件时，没有为特定参数实例化模板指明定义。

\subsubsubsection{9.1.2\hspace{0.2cm}头文件中的模板}

对于这个问题，常见的解决方案是使用与宏或内联函数相同的方法:在声明模板的头文件中包含模板的定义。

也就是，不提供文件myfirst.cpp，而是重写myfirst.hpp，使其包含所有模板声明和模板定义:

\hspace*{\fill} \\ %插入空行
\noindent
\textit{basics/myfirst2.hpp}
\begin{lstlisting}[style=styleCXX]
#ifndef MYFIRST_HPP
#define MYFIRST_HPP

#include <iostream>
#include <typeinfo>

// declaration of template
template<typename T>
void printTypeof (T const&);

// implementation/definition of template
template<typename T>
void printTypeof (T const& x)
{
	std::cout << typeid(x).name() << ’\n’;
}

#endif // MYFIRST_HPP
\end{lstlisting}

这种组织模板的方法称为“包含模型”，程序可以正确地编译、链接和执行。

这种方法大大增加了包含头文件myfirst.hpp的成本。本例中，代价是必须包含模板定义所使用的头文件——本例中是<iostream>和<typeinfo>。这相当于多了数万行代码，因为像<iostream>这样的头文件其中包含许多模板定义。

这在实践中是一个问题，因为它增加了编译器编译重要程序所需的时间。因此，我们将研究一些方法来解决这个问题，包括预编译头文件(参见9.3节)和使用显式模板实例化(参见14.5节)。

尽管存在构建时问题，但仍然建议尽可能使用“包含模型”的方式来组织模板代码，除非有更好的机制可用。写这本书时，这样的机制已经有了:模块，在17.11节中会介绍。它们是一种语言机制，允许开发者以某种方式组织代码:编译器可以单独编译所有声明，然后在需要时有效、有选择地导入处理过的声明。

关于“包含模型”的另一个(更微妙的)结果，非内联函数模板与内联函数和宏有所不同，它们没有在调用点展开。当实例化时，会创建函数副本。因为这是一个自动过程，所以编译器最终可能会在两个不同的文件中创建两个副本，而一些链接器在发现同一个函数的两个不同定义时可能会发出错误。理论上，这不是我们所关心的问题:这是C++编译系统要解决的问题。实践中，大多数情况下都没问题，根本不需要处理这个问题。然而，对于创建代码库的大型项目，偶尔会出现这种奇怪的问题。第14章中对实例化方案的讨论，并对C++翻译系统(编译器)文档的研究应该有助于解决这些问题。

最后，示例中适用于普通函数模板的内容，也适用于成员函数和类模板的静态数据成员，以及成员函数模板。












